/*
 * Copyright (c) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { cert } from '@kit.DeviceCertificateKit';
import { HostnameVerifier } from '../HostnameVerifier';
import { SSLSession } from '../Socket/SSLSocket';
import { canParseAsIpAddress, isAscii, toCanonicalHost } from '../utils/StringUtil';

/**
 * A HostnameVerifier consistent with RFC 2818.
 * @see http://www.ietf.org/rfc/rfc2818.txt
 */
export default class OkHostnameVerifier implements HostnameVerifier {
  public static INSTANCE: OkHostnameVerifier = new OkHostnameVerifier();

  private static readonly ALT_DNS_NAME: number = 2;
  private static readonly ALT_IPA_NAME: number = 7;

  constructor() {}

  async verify(host: string, session: SSLSession): Promise<boolean> {
    if (!isAscii(host)) {
      return false;
    }
    try {
      const remoteCert = await session.getRemoteCertificate();
      return await this.verifyCertificate(host, remoteCert);
    } catch (e) {
      return false;
    }
  }

  async verifyCertificate(host: string, certificate: cert.EncodingBlob): Promise<boolean> {
    if (canParseAsIpAddress(host)) {
      return await this.verifyIpAddress(host, certificate);
    } else {
      return await this.verifyHostname(host, certificate);
    }
  }

  /** Returns true if certificate matches ipAddress. */
  private async verifyIpAddress(ipAddress: string, certificate: cert.EncodingBlob): Promise<boolean> {
    const canonicalIpAddress = toCanonicalHost(ipAddress);
    if (!canonicalIpAddress) {
      return false;
    }

    const altNames = await this.getSubjectAltNames(certificate, OkHostnameVerifier.ALT_IPA_NAME);
    return altNames.some(altName => canonicalIpAddress === toCanonicalHost(altName));
  }

  /** Returns true if certificate matches hostname. */
  private async verifyHostname(hostname: string, certificate: cert.EncodingBlob): Promise<boolean> {
    const lowercaseHostname = this.asciiToLowercase(hostname);
    const altNames = await this.getSubjectAltNames(certificate, OkHostnameVerifier.ALT_DNS_NAME);
    return altNames.some(altName => this.verifyHostnamePattern(lowercaseHostname, altName));
  }

  /**
   * This is like toLowerCase() except that it does nothing if this contains any non-ASCII
   * characters. We want to avoid lower casing special chars like U+212A (Kelvin symbol).
   */
  private asciiToLowercase(str: string): string {
    return isAscii(str) ? str.toLowerCase() : str;
  }

  /**
   * Returns true if hostname matches the domain name pattern.
   *
   * @param hostname lower-case host name.
   * @param pattern domain name pattern from certificate. May be a wildcard pattern such as *.android.com.
   */
  public verifyHostnamePattern(hostname: string | null, pattern: string | null): boolean {
    // Basic sanity checks
    if (hostname === null || hostname === undefined || hostname.length === 0 || hostname.startsWith(".") || hostname.endsWith("..")) {
      return false;
    }
    if (pattern === null || pattern === undefined || pattern.length === 0 || pattern.startsWith(".") || pattern.endsWith("..")) {
      return false;
    }

    // Normalize to absolute domain names
    if (!hostname.endsWith(".")) {
      hostname += ".";
    }
    if (!pattern.endsWith(".")) {
      pattern += ".";
    }

    pattern = this.asciiToLowercase(pattern);

    // Not a wildcard pattern -- hostname and pattern must match exactly
    if (pattern.indexOf("*") === -1) {
      return hostname === pattern;
    }

    // Wildcard pattern rules:
    // 1. Asterisk (*) is only permitted in the left-most domain name label
    // 2. Asterisk (*) cannot match across domain name labels
    // 3. Wildcard patterns for single-label domain names are not permitted

    if (!pattern.startsWith("*.") || pattern.indexOf("*", 1) !== -1) {
      return false;
    }

    if (hostname.length < pattern.length) {
      return false;
    }

    if (pattern === "*.") {
      return false;
    }

    // Hostname must end with the region of pattern following the asterisk
    const suffix = pattern.substring(1);
    if (!hostname.endsWith(suffix)) {
      return false;
    }

    // Check that asterisk did not match across domain name labels
    const suffixStartIndexInHostname = hostname.length - suffix.length;
    if (suffixStartIndexInHostname > 0 && hostname.lastIndexOf(".", suffixStartIndexInHostname - 1) !== -1) {
      return false;
    }

    return true;
  }

  /**
   * Returns all subject alternative names from certificate.
   */
  public static async allSubjectAltNames(certificate: cert.X509Cert): Promise<string[]> {
    const altIpaNames = await this.getSubjectAltNamesFromCert(certificate, OkHostnameVerifier.ALT_IPA_NAME);
    const altDnsNames = await this.getSubjectAltNamesFromCert(certificate, OkHostnameVerifier.ALT_DNS_NAME);
    return [...altIpaNames, ...altDnsNames];
  }

  /**
   * Get subject alternative names from X509Cert object.
   *
   * 首先尝试从 getItem(EXTENSIONS) 解析完整的 ASN.1 数据（推荐，无 bug）
   * 如果失败，则回退到 getSubjectAltNames()（可能有数据截断问题）
   */
  private static async getSubjectAltNamesFromCert(certificate: cert.X509Cert, type: number): Promise<string[]> {
    // 首先尝试从 Extensions 解析（完整数据，无 bug）
    try {
      const result = this.parseSubjectAltNamesFromExtensions(certificate, type);
      if (result.length > 0) {
        return result;
      }
    } catch (e) {
      console.warn('[OkHostnameVerifier] Failed to parse from extensions:', e);
    }

    // 回退到 getSubjectAltNames()（可能有数据截断问题）
    return this.parseSubjectAltNamesFromApi(certificate, type);
  }

  /**
   * 从 getItem(EXTENSIONS) 解析 Subject Alternative Names
   * 这个方法返回完整的 ASN.1 编码数据，不会有数据截断问题
   *
   * ASN.1 结构:
   * - SEQUENCE (Extensions)
   *   - SEQUENCE (Extension)
   *     - OID 2.5.29.17 (subjectAltName)
   *     - OCTET STRING
   *       - SEQUENCE (GeneralNames)
   *         - [2] dNSName (tag 0x82)
   *         - [7] iPAddress (tag 0x87)
   */
  private static parseSubjectAltNamesFromExtensions(certificate: cert.X509Cert, type: number): string[] {
    const result: string[] = [];

    try {
      const extensions = certificate.getItem(cert.CertItemType.CERT_ITEM_TYPE_EXTENSIONS);
      if (!extensions || !extensions.data || extensions.data.length === 0) {
        return result;
      }

      const data = extensions.data;
      const expectedTag = 0x80 | type; // 0x82 for DNS, 0x87 for IP

      // 搜索 subjectAltName OID: 2.5.29.17 = 55 1d 11
      const sanOid = [0x55, 0x1d, 0x11];
      let sanOffset = this.findSequence(data, sanOid);

      if (sanOffset === -1) {
        return result;
      }

      // 找到 OID 后，跳过它，然后找到 OCTET STRING (tag 0x04)
      let offset = sanOffset + sanOid.length;

      // 跳过可能的 BOOLEAN (critical flag)
      if (offset < data.length && data[offset] === 0x01) {
        offset += 3; // BOOLEAN tag + length + value
      }

      // 找到 OCTET STRING
      if (offset >= data.length || data[offset] !== 0x04) {
        return result;
      }
      offset++; // 跳过 tag

      // 读取 OCTET STRING 长度
      const octetLen = this.readAsn1Length(data, offset);
      offset = octetLen.nextOffset;

      // OCTET STRING 内部是 SEQUENCE (GeneralNames)
      if (offset >= data.length || data[offset] !== 0x30) {
        return result;
      }
      offset++; // 跳过 SEQUENCE tag

      const seqLen = this.readAsn1Length(data, offset);
      offset = seqLen.nextOffset;
      const endOffset = offset + seqLen.length;

      // 解析 GeneralNames
      while (offset < endOffset && offset < data.length) {
        const tag = data[offset];
        offset++;

        const lenInfo = this.readAsn1Length(data, offset);
        offset = lenInfo.nextOffset;
        const valueLen = lenInfo.length;

        if (tag === expectedTag && offset + valueLen <= data.length) {
          const valueData = data.slice(offset, offset + valueLen);

          if (type === OkHostnameVerifier.ALT_DNS_NAME) {
            const decoded = this.uint8ArrayToString(valueData);
            if (decoded) {
              result.push(decoded);
            }
          } else if (type === OkHostnameVerifier.ALT_IPA_NAME) {
            const decoded = this.decodeIpAddress(valueData);
            if (decoded) {
              result.push(decoded);
            }
          }
        }

        offset += valueLen;
      }
    } catch (e) {
      console.warn('[OkHostnameVerifier] Error parsing extensions:', e);
    }

    return result;
  }

  /**
   * 在数据中查找指定序列
   */
  private static findSequence(data: Uint8Array, sequence: number[]): number {
    for (let i = 0; i <= data.length - sequence.length; i++) {
      let found = true;
      for (let j = 0; j < sequence.length; j++) {
        if (data[i + j] !== sequence[j]) {
          found = false;
          break;
        }
      }
      if (found) {
        return i;
      }
    }
    return -1;
  }

  /**
   * 读取 ASN.1 长度字段
   */
  private static readAsn1Length(data: Uint8Array, offset: number): { length: number; nextOffset: number } {
    if (offset >= data.length) {
      return { length: 0, nextOffset: offset };
    }

    let length = data[offset];
    let nextOffset = offset + 1;

    if (length > 127) {
      // 长格式
      const numBytes = length & 0x7f;
      length = 0;
      for (let i = 0; i < numBytes && nextOffset < data.length; i++) {
        length = (length << 8) | data[nextOffset];
        nextOffset++;
      }
    }

    return { length, nextOffset };
  }

  /**
   * 从 getSubjectAltNames() API 解析（可能有数据截断问题）
   */
  private static parseSubjectAltNamesFromApi(certificate: cert.X509Cert, type: number): string[] {
    try {
      const subjectAltNames = certificate.getSubjectAltNames();
      if (!subjectAltNames || !subjectAltNames.data) {
        return [];
      }

      const result: string[] = [];

      for (const data of subjectAltNames.data) {
        if (!data || data.length === 0) {
          continue;
        }

        // 移除末尾的 null 终止符
        let effectiveLength = data.length;
        if (data[data.length - 1] === 0) {
          effectiveLength = data.length - 1;
        }

        if (effectiveLength === 0) {
          continue;
        }

        // 根据请求的类型和数据特征判断
        if (type === OkHostnameVerifier.ALT_DNS_NAME) {
          if (this.isAsciiString(data, effectiveLength)) {
            const decoded = this.uint8ArrayToString(data.slice(0, effectiveLength));
            if (decoded) {
              result.push(decoded);
            }
          }
        } else if (type === OkHostnameVerifier.ALT_IPA_NAME) {
          if (effectiveLength === 4 || effectiveLength === 16) {
            const decoded = this.decodeIpAddress(data.slice(0, effectiveLength));
            if (decoded) {
              result.push(decoded);
            }
          }
        }
      }
      return result;
    } catch (e) {
      console.warn('[OkHostnameVerifier] Failed to get subject alt names from API:', e);
      return [];
    }
  }

  /**
   * Check if data is an ASCII string (printable characters).
   */
  private static isAsciiString(data: Uint8Array, length: number): boolean {
    for (let i = 0; i < length; i++) {
      const byte = data[i];
      if (byte < 32 || byte > 126) {
        return false;
      }
    }
    return true;
  }

  /**
   * Get subject alternative names from encoded certificate.
   */
  private async getSubjectAltNames(certificate: cert.EncodingBlob, type: number): Promise<string[]> {
    try {
      const x509Cert = await cert.createX509Cert(certificate);
      return OkHostnameVerifier.getSubjectAltNamesFromCert(x509Cert, type);
    } catch (e) {
      return [];
    }
  }

  /**
   * Decode IP address from bytes.
   */
  private static decodeIpAddress(data: Uint8Array): string | null {
    if (data.length === 4) {
      // IPv4
      return `${data[0]}.${data[1]}.${data[2]}.${data[3]}`;
    } else if (data.length === 16) {
      // IPv6
      const parts: string[] = [];
      for (let i = 0; i < 16; i += 2) {
        const value = (data[i] << 8) | data[i + 1];
        parts.push(value.toString(16));
      }
      return parts.join(":");
    }
    return null;
  }

  /**
   * Convert Uint8Array to string (UTF-8).
   */
  private static uint8ArrayToString(data: Uint8Array): string {
    let result = "";
    for (let i = 0; i < data.length; i++) {
      result += String.fromCharCode(data[i]);
    }
    return result;
  }
}
